查看原文
其他

伴随Simplex噪声的GPU粒子动画

Unity Unity官方平台 2019-05-08

我们将继续分享加拿大游戏特效大神Mirza Beig的粒子系统的系列教程,我们将在上一篇《Unity自定义粒子顶点流》的基础上,学习在Unity中实现伴随Simplex噪声的GPU粒子动画。


在《Unity自定义粒子顶点流》教程中,我们学习了如何制作基础的粒子着色器,让它读取和使用从正确配置的粒子系统发送的顶点流,最后我们得到了富有弹性的弹簧粒子效果,该效果不是很有创意,也并不惊艳。


本教程中,我们将在此基础上更进一步,实现下图的效果。



如果你已经完成了上一个的教程,并拥有《创建3D均匀粒子网格》教程的脚本,那么本教程中特效的制作过程会非常简单。


本教程中我们会对每个粒子的中心传递额外的3D顶点流,然后使用该顶点流决定来自噪声函数的顶点偏移。本文将使用的噪声函数为Simplex噪声,类似柏林噪声。


Part 1:粒子系统设置

首先设置一个基础粒子系统,我们可以用它来测试效果。我们将使用默认粒子纹理,最后的结果类似下图。

 

 

首先,创建一个粒子系统。

 

 

重置Transform组件,然后按下图设置Main模块。勾选了Prewarm,设置Start Speed为0,并将Start Size设为在0.25和1之间随机取值,然后设置Max Particles为10,000。

 

 

设置Emission模块的Rate over Time为2,000。

 

 

将Shape设为Box,Scale设为[50, 0, 50]。

  


现在我们应该会得到一个反复浮动的平坦粒子层。

 

 

我们需要使生成效果更平滑,现在启用Color over Lifetime模块,按照下图设置梯度,从而浅入淡出粒子。

 


在Renderer模块中,启用Custom Vertex Streams并添加Center流。

 

 

这样会造成TEXCOORD1的溢出,我们会在处理着色器时解决该问题,这样就完成了测试粒子系统。

 

Part 2:基本着色器

在这一部分,我们将创建基本着色器,用来处理额外的顶点流数据。该着色器将基于我们在《Unity自定义粒子顶点流》中创建的基础无光粒子着色器,下面的代码以供参考。

Shader "Unlit/Simple Particle Unlit"

{

    Properties

    {

        _MainTex("Texture", 2D) = "white" {}

    }

 

    SubShader

    {

        Tags { "Queue" = "Transparent" "RenderType" = "Opaque" }

        LOD 100

 

        Blend One One // 加法混合

        ZWrite Off // 关闭深度测试

 

        Pass

        {

            CGPROGRAM

            #pragma vertex vert

            #pragma fragment frag

            // 实现模糊效果

            #pragma multi_compile_fog

 

            #include "UnityCG.cginc"

 

            struct appdata

            {

                float4 vertex : POSITION;

                fixed4 color : COLOR;

                float3 tc0 : TEXCOORD0;

            };

 

            struct v2f

            {

                float3 tc0 : TEXCOORD0;

                UNITY_FOG_COORDS(1)

                float4 vertex : SV_POSITION;

                fixed4 color : COLOR;

            };

 

            sampler2D _MainTex;

            float4 _MainTex_ST;

 

            v2f vert(appdata v)

            {

                v2f o;

 

                float3 vertexOffset = 0;

 

                v.vertex.xyz += vertexOffset;

                o.vertex = UnityObjectToClipPos(v.vertex);

 

                //从保存在颜色顶点输入的粒子系统接收数据,并将该数据用于初始化颜色

 

                o.color = v.color;

 

                o.tc0.xy = TRANSFORM_TEX(v.tc0, _MainTex);

 

                //初始化tex coord变量

 

                o.tc0.z = v.tc0.z;

 

                UNITY_TRANSFER_FOG(o,o.vertex);

                return o;

            }

 

            fixed4 frag(v2f i) : SV_Target

            {

                // 采样纹理

                fixed4 col = tex2D(_MainTex, i.tc0);

 

                // 让纹理颜色和粒子系统的顶点颜色输入相乘.

 

                col *= i.color; 

                col *= col.a; 

 

                // 应用模糊效果

                UNITY_APPLY_FOG(i.fogCoord, col);

                return col;

            }

            ENDCG

        }

    }

}


我们需要修改一部分内容,首先是名称。

Shader "Unlit/Simplex Noise Particle Unlit"


将tc0之前为uv,定义为float4,并添加TEXCOORD1为tc1,。这样允许我们通过使用该着色器,传入并保存更多来自粒子系统的自定义顶点流数据。

struct appdata

{

    float4 vertex : POSITION;

    fixed4 color : COLOR;

    float4 tc0 : TEXCOORD0;

    float4 tc1 : TEXCOORD1;

};

 

struct v2f

{

    float4 tc0 : TEXCOORD0;

    float4 tc1 : TEXCOORD1;

    UNITY_FOG_COORDS(1)

    float4 vertex : SV_POSITION;

    fixed4 color : COLOR;

};


接下来,我们初始化导出的tc0.zw到tc0.zw输入中的内容。


我们指定了z和w,因为xy已经被指定为纹理的UV坐标。我们可以将整个tc1输入转储到tc1输出,因为在顶点程序中,这部分数据不和之前的任何操作共享。

v2f vert (appdata v)

{

    v2f o;

 

    float3 vertexOffset = 0;

 

    v.vertex.xyz += vertexOffset;

    o.vertex = UnityObjectToClipPos(v.vertex);

 

    // 从保存在颜色顶点输入的粒子系统接收数据,并将该数据用于初始化颜色

    o.color = v.color;

    o.tc0.xy = TRANSFORM_TEX(v.tc0, _MainTex);

     

    // 初始化tex coord变量

    o.tc0.zw = v.tc0.zw;

    o.tc1 = v.tc1;

 

    UNITY_TRANSFER_FOG(o,o.vertex);

    return o;

}


我们可以使用该着色器处理上一部分使用的粒子系统,而且不必看到烦人的不匹配流警告。

 

我们返回Unity编辑器中,创建一个新材质,为其指定着色器,然后设置粒子系统Renderer模块的材质。指定Unity的默认粒子纹理。



如果一切顺利,我们应该不会看到任何警告,而且粒子系统会按照之前的方法渲染。

 

下面是完整的基本着色器代码。

Shader "Unlit/Simplex Noise Particle Unlit"

{

    Properties

    {

        _MainTex ("Texture", 2D) = "white" {}

    }

 

    SubShader

    {

        Tags { "Queue" = "Transparent" "RenderType" = "Opaque" }

        LOD 100

 

        Blend One One // 加法混合

        ZWrite Off // 关闭深度测试

 

        Pass

        {

            CGPROGRAM

            #pragma vertex vert

            #pragma fragment frag

            // 实现模糊效果

            #pragma multi_compile_fog

 

            #include "UnityCG.cginc"

 

            struct appdata

            {

                float4 vertex : POSITION;

                fixed4 color : COLOR;

                float4 tc0 : TEXCOORD0;

                float4 tc1 : TEXCOORD1;

            };

 

            struct v2f

            {

                float4 tc0 : TEXCOORD0;

                float4 tc1 : TEXCOORD1;

                UNITY_FOG_COORDS(1)

                float4 vertex : SV_POSITION;

                fixed4 color : COLOR;

            };

 

            sampler2D _MainTex;

            float4 _MainTex_ST;

                         

            v2f vert (appdata v)

            {

                v2f o;

                 

                float3 vertexOffset = 0;

 

                v.vertex.xyz += vertexOffset;

                o.vertex = UnityObjectToClipPos(v.vertex);

 

                // 从保存在颜色顶点输入的粒子系统接收数据,并将该数据用于初始化颜色。

                o.color = v.color;

                o.tc0.xy = TRANSFORM_TEX(v.tc0, _MainTex);

 

                // 初始化tex coord变量

                o.tc0.zw = v.tc0.zw;

                o.tc1 = v.tc1;

 

                UNITY_TRANSFER_FOG(o,o.vertex);

                return o;

            }

 

            fixed4 frag (v2f i) : SV_Target

            {

                // 采样纹理

                fixed4 col = tex2D(_MainTex, i.tc0);

 

                // 让纹理颜色和粒子系统的顶点颜色输入相乘

                col *= i.color;

                col *= col.a;

 

                // 应用模糊效果

                UNITY_APPLY_FOG(i.fogCoord, col);

                return col;

            }

            ENDCG

        }

    }

}

Part 3:Simplex噪声(顶点动画)

这一部分是本教程的重点。我们将在此使用Keijiro Takahashi的HLSL实现方法在Unity实现Simplex噪声。


请访问GitHub下载代码库,它包含多个不同的噪声函数,但我们只需要3D Simplex噪声,即SimplexNoise3D.hlsl。

 

下载噪声代码库:

https://github.com/keijiro/NoiseShader/blob/master/Assets/HLSL/SimplexNoise3D.hlsl


为了能够在着色器中使用噪声函数,我们需要通过include引用该代码。

#include "UnityCG.cginc"

#include "SimplexNoise3D.hlsl"


我们将添加一些参数,用来控制噪声速度,即在3D空间中滚动偏移的速度、频率、振幅和负范围限定。我们将这些参数作为属性添加到着色器中。

_MainTex ("Texture", 2D) = "white" {}

 

_NoiseSpeedX("Noise Speed X", Range(0 , 100)) = 0.0

_NoiseSpeedY("Noise Speed Y", Range(0 , 100)) = 0.0

_NoiseSpeedZ("Noise Speed Z", Range(0 , 100)) = 1.0

 

_NoiseFrequency("Noise Frequency", Range(0 , 1)) = 0.1

_NoiseAmplitude("Noise Amplitude", Range(0 , 10)) = 2.0

 

_NoiseAbs("Noise Abs", Range(0 , 1)) = 1.0


我们还需要添加匹配的着色器变量。

sampler2D _MainTex;

float4 _MainTex_ST;

 

float _NoiseSpeedX;

float _NoiseSpeedY;

float _NoiseSpeedZ;

 

float _NoiseFrequency;

float _NoiseAmplitude;

 

float _NoiseAbs;


现在我们可以处理顶点部分即在添加偏移前,从而为粒子设置动画。请记住,粒子的中心向量在TEXCOORD0和TEXCOORD1中混合。


 

中心向量会转换为存在tc0.zw的中心X和Y,以及存在tc0.x的Z。你可能会想,如果我们要像之前教程那样偏移顶点,为什么我们还要传入Center顶点流?

 

这是因为粒子顶点会根据粒子的世界空间位置在最初被偏移,然后用作噪声函数的输入。随着大量粒子在足够大的区域中扩散,我们可以看到平滑的噪声分布,并使它随着时间变化。

 

我们不能直接传入顶点位置,因为粒子是带有至少三个顶点的多边形,每个顶点都是空间中的不同点。如果我们使用了顶点,那么我们要根据顶点获取不同的偏移,这样在网格变形时,会得到非常奇怪的粒子。


这不是我们想要的效果,我们希望整个粒子进行移动,这意味着要以相同的偏移来一起移动它的所有顶点,所以我们使用粒子的中心位置。

float3 particleCenter = float3(v.tc0.zw, v.tc1.x);


我们可以通过time * speedZ计算出3D噪声偏移。虽然我们可以为所有轴添加单独的噪声速度,但我们不想让噪声在X轴和Y轴变化。

float3 noiseOffset = _Time.y * float3(_NoiseSpeedX, _NoiseSpeedY, _NoiseSpeedZ);


现在我们可以计算单维噪声值。首先传入particleCenter,添加偏移,然后将结果乘以频率属性。

float noise = snoise((particleCenter + noiseOffset) * _NoiseFrequency);


snoise函数会提供范围在[-1.0, 1.0]的数值。因为我们想要将该数值重映射为[0.0, 1.0]的范围,然后在范围间进行混合,所以我们可以添加代码来实现这个过程,代码首先会重新映射,然后使用lerp函数来进行混合。

float noise01 = (noise + 1.0) / 2.0;

float noiseRemap = lerp(noise, noise01, _NoiseAbs);


顶点偏移会在世界空间Y中计算noiseRemap * _NoiseAmplitude 。

float3 vertexOffset = float3(0.0, noiseRemap * _NoiseAmplitude, 0.0);

v.vertex.xyz += vertexOffset;


顶点动画的处理到这就完成了,下一部分我们将会根据噪声值设置颜色的变化。我们返回到Unity中查看结果,下面的示例效果可以通过调整粒子系统Main模块的Start Color来实现。


Part4:Simplex噪声(片段/像素颜色动画)

我们已经拥有代码来根据噪声制作顶点动画,所以这部分差不多就要完成了。首先添加二个额外属性和全局变量,用于根据重新映射的[0.0, 1.0]范围噪声值来处理插补的颜色。

 

材质属性会添加到着色器文件的顶部,,HDR标签能确保我们可以大幅增大进入HDR范围的强度,从而允许泛光等后期处理效果能和着色器搭配使用。

[HDR] _ColourA("Color A", Color) = (0,0,0,0)

[HDR] _ColourB("Color B", Color) = (1,1,1,1)


着色器变量添加在噪声变量下。

float4 _ColourA;

float4 _ColourB;


最后在片段部分,在将col乘以输入顶点颜色的代码和将col乘以结合Alpha值的代码之间,添加下面的代码。

col *= i.color;

 

float3 particleCenter = float3(i.tc0.zw, i.tc1.x);

float3 noiseOffset = _Time.y * float3(_NoiseSpeedX, _NoiseSpeedY, _NoiseSpeedZ);

 

float noise = snoise((particleCenter + noiseOffset) * _NoiseFrequency);

float noise01 = (noise + 1.0) / 2.0;

 

col = lerp(col * _ColourA, col * _ColourB, noise01);

col *= col.a;


你可以注意到,大部分代码是从顶点部分复制粘贴得来,保存脚本,使用i.tc0和i.tc1来从输入获取流,并使用lerp函数来在颜色间插补。

 

这些代码只是少量修改了顶点动画部分的代码,下面是得到的效果。



这样我们就处理好着色器了,在下一部分,我们将使用现有的代码实现预览图像。下面是完整的着色器代码。

Shader "Unlit/Simplex Noise Particle Unlit"

{

    Properties

    {

        _MainTex ("Texture", 2D) = "white" {}

 

        _NoiseSpeedX("Noise Speed X", Range(0 , 100)) = 0.0

        _NoiseSpeedY("Noise Speed Y", Range(0 , 100)) = 0.0

        _NoiseSpeedZ("Noise Speed Z", Range(0 , 100)) = 1.0

 

        _NoiseFrequency("Noise Frequency", Range(0 , 1)) = 0.1

        _NoiseAmplitude("Noise Amplitude", Range(0 , 10)) = 2.0

 

        _NoiseAbs("Noise Abs", Range(0 , 1)) = 1.0

 

        [HDR] _ColourA("Color A", Color) = (0,0,0,0)

        [HDR] _ColourB("Color B", Color) = (1,1,1,1)

    }

 

    SubShader

    {

        Tags { "Queue" = "Transparent" "RenderType" = "Opaque" }

        LOD 100

 

        Blend One One // 加法混合

        ZWrite Off // 关闭深度测试

 

        Pass

        {

            CGPROGRAM

            #pragma vertex vert

            #pragma fragment frag

            // 实现模糊效果

            #pragma multi_compile_fog

 

            #include "UnityCG.cginc"

            #include "SimplexNoise3D.hlsl"

 

            struct appdata

            {

                float4 vertex : POSITION;

                fixed4 color : COLOR;

                float4 tc0 : TEXCOORD0;

                float4 tc1 : TEXCOORD1;

            };

 

            struct v2f

            {

                float4 tc0 : TEXCOORD0;

                float4 tc1 : TEXCOORD1;

                UNITY_FOG_COORDS(1)

                float4 vertex : SV_POSITION;

                fixed4 color : COLOR;

            };

 

            sampler2D _MainTex;

            float4 _MainTex_ST;

 

            float _NoiseSpeedX;

            float _NoiseSpeedY;

            float _NoiseSpeedZ;

 

            float _NoiseFrequency;

            float _NoiseAmplitude;

 

            float _NoiseAbs;

 

            float4 _ColourA;

            float4 _ColourB;

                         

            v2f vert (appdata v)

            {

                v2f o;

 

                float3 particleCenter = float3(v.tc0.zw, v.tc1.x);

                float3 noiseOffset = _Time.y * float3(_NoiseSpeedX, _NoiseSpeedY, _NoiseSpeedZ);

 

                float noise = snoise((particleCenter + noiseOffset) * _NoiseFrequency);

 

                float noise01 = (noise + 1.0) / 2.0;

                float noiseRemap = lerp(noise, noise01, _NoiseAbs);

                 

                float3 vertexOffset = float3(0.0, noiseRemap * _NoiseAmplitude, 0.0);

 

                v.vertex.xyz += vertexOffset;

                o.vertex = UnityObjectToClipPos(v.vertex);

 

                // 从保存在颜色顶点输入的粒子系统接收数据,并将该数据用于初始化颜色

                o.color = v.color;

                o.tc0.xy = TRANSFORM_TEX(v.tc0, _MainTex);

 

                // 初始化tex coord变量

                o.tc0.zw = v.tc0.zw;

                o.tc1 = v.tc1;

 

                UNITY_TRANSFER_FOG(o,o.vertex);

                return o;

            }

 

            fixed4 frag (v2f i) : SV_Target

            {

                //采样纹理

                fixed4 col = tex2D(_MainTex, i.tc0);

 

                //让纹理颜色和粒子系统的顶点颜色输入相乘

                col *= i.color;

 

                float3 particleCenter = float3(i.tc0.zw, i.tc1.x);

                float3 noiseOffset = _Time.y * float3(_NoiseSpeedX, _NoiseSpeedY, _NoiseSpeedZ);

 

                float noise = snoise((particleCenter + noiseOffset) * _NoiseFrequency);

                float noise01 = (noise + 1.0) / 2.0;

 

                col = lerp(col * _ColourA, col * _ColourB, noise01); 

                col *= col.a;

 

                // 应用模糊效果

                UNITY_APPLY_FOG(i.fogCoord, col);

                return col;

            }

            ENDCG

        }

    }

}

Part 5:组合所有内容

最后这一部分,我们需要使用《创建3D均匀粒子网格》教程中的脚本。我们将创建三个粒子系统,每个都有不同的材质和设置,以及相同的纹理。

 

三个粒子系统都将添加粒子网格脚本,设置如下图所示。


 

除了Renderer模块外,所有模块都要禁用。发射,颜色和形状都由附加的脚本控制。

 

 

所有粒子系统Main模块的设置如下。即Start Lifetime设为1,000,Start Speed设为0,Start Size设为0.5,Max Particles设为100,000。

 

 

确保启用Custom Vertex Streams,并添加Center流。

 


粒子系统1

粒子系统1是在背景向下滚动的绿色网格。


 

Transform组件按下图进行设置,Position Z设为10,Rotation X为110,Scale设为2, 1, 1.5。

 

 

Noise Speed的XYZ值分别为(0, 1, 0),Noise Frequency为0.25,Noise Amplitude为0.75,Noise Abs为1。二个颜色分别为蓝色(Alpha = 10)和绿色(Alpha = 75)。

 

 

粒子系统2

粒子系统2是发光的红色3D波形层。

 

 

按照下图设置Transform组件,Position的XYZ值分别为(0, -1.25, 8),Rotation的XYZ值都为0,Scale的XYZ值分别为(3, 0.5, 1)。

 

 

设置Render Mode为Mesh,然后设置Mesh为Cube。

 


该系统材质的唯一变化为颜色。颜色都为红色,Alpha值分别为0和25。

 

 

粒子系统3

粒子系统3是粒子系统2的副本,我们会再次使用立方体网格发射器来创建蓝色的波形层。

 

 

我们需要设置Transform属性,Position的XYZ值分别为(0, -3.15, 10)。Rotation的XYZ值为0。Scale的XYZ值分别为(12, 1, 1)。


  

最后,我们需要修改材质颜色为深蓝色和湖绿色,不透明度分别为5和25。



最终效果

现在完成了整个教程,我们会得到类似下图的效果。


小结

在Unity中实现伴随Simplex噪声的GPU粒子动画就学习到这里,虽然效果依旧不是特别精美,但是这为后续教程中实现更为复杂的精美粒子特效奠定基础,也帮助我们熟练掌握噪音在粒子系统中的使用。


更多Unity教程,尽在Unity官方中文论坛(UnityChina.cn)。


原文来源:mirzabeig.com


粒子特效教程


官方活动

Unite Shanghai 2019暖冬特惠票限量销售中

2019年5月10日-12日上海,Unite大会强势回归。 暖冬特惠票正式开售,购票即获指定Asset Store资源商店精品21款资源的5折优惠券。[了解详情...

购票链接:

http://UniteShanghai2019.bagevent.com


Unity全球学生开发挑战赛火热进行中最后一周

Unity面向全球的学生推出-Unity全球学生开发挑战赛,寻找全世界最具创意,展现自我的学生开发者团队。[了解详情...

活动地址:

https://connect.unity.com/challenges/gsc2018


活动 | Unity for Humanity 2019挑战赛

参加Unity for Humanity挑战赛,向我们展示你的作品将如何改变世界。[了解详情...]

活动地址:

https://connect.unity.com/challenges/unityforhumanity


点击“阅读原文”访问Unity官方中文论坛

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存